Repository navigation
fix(tasks): a refused track_tasks batch leaves no card behind (#2597) - #2602
Merged
Merged
Conversation
track_tasks ran its items one at a time and every makeTask() put the new card into store() immediately, while the batch save() ran only after the loop. A later item's refusal — the ask + column: 'done' pair, a queue already at its cap, a column the card cannot move to — unwound with the earlier cards still on the board, so the caller was told the batch had failed while those cards stayed. The list now runs against a staged copy of the board and the copy is committed only after the last item passed: a refusal leaves state.tasks byte-identical, no card is created, and no claim line is posted for a card that never landed. Every per-item refusal is unchanged and fires in the same order. Fixes #2597 Docs-not-needed: validation ordering inside an existing handler; no subsystem behaviour changes
Collaborator
Author
|
Verdict: PASS at 7ca7534 Ran at this tip: Not run: What I checked:
|
Birfy
pushed a commit
to HugoXK/FlyBot
that referenced
this pull request
Nov 10, 2026
…t card survives the error (ThinkFlowLab#2763) Fixes ThinkFlowLab#2761 Owner: Dev-1 Written-by: Dev-1 Bot: Dev-1 Claim-Id: 0cbab9084d364946a62fcc0a24fb58d3 Base: f2ef4df Risk tier: T2 ## What changed `track_tasks` reached `state.tasks` before it saved: the main batch committed its staged copy into the board at the end of the loop, and the four early paths (`claim: true`, the handoff pool, `rework`, `release`) mutated the live card in place; the single `save()` came after. A store write that refuses — the temp name is not a plain file of its own, the state lock was lost, the disk is full — therefore left cards on the board the file never took, and the next unrelated save wrote them out. The probe in the issue reproduces it with a soft-linked temp name: `track_tasks` answers `Not saved: …`, `state.tasks` already shows `Ghost card`, and one later `track_tasks` puts both cards in `state.json`. After: each of the five save points snapshots the board first and puts it back when the store refuses, then rethrows the store's own error. The restore deletes a card the batch created, puts back a card the batch removed, and rewrites every key of a card the batch changed in the snapshot's order, so `JSON.stringify(rt.state.tasks)` is byte-identical to the call's start. `state.revision` is not restored: the store increments it before the write, and a refused write does not rewind it. The room claim line (`announceClaim` → `postClaimLine`) moved after a successful save, so a refused claim posts nothing. **The snapshot is deep, not the shallow `{ ...state.tasks }` the issue sketches.** A shallow copy is enough for the main path alone, because that path mutates a `structuredClone` staging copy (ThinkFlowLab#2597); it is not enough for the four early paths, which mutate the live card (`live.claimedBy = botId`, `openRework(task, …)`, `requeue(task, …)`), so a shallow snapshot holds the very objects the rollback has to undo. The test matrix below fails 4 of 7 cases if the snapshot is shallow (claim, release, rework, and the claim-line half), which is why this PR deviates from that note. Restoring writes the old references back into the same keys, so a caller already holding a card keeps seeing that object. `revision` monotonicity and `saves.length === 1` are asserted, and the refusal is a real store refusal (`tempWritable`), not a stubbed save: the temp name is linked to a file outside the home, whose bytes are read back to prove nothing was written through it. ## Behaviour change: `claimCue` and the claim's own save `postClaimLine` records `claimCue` on the card so an identical line is not posted twice (ThinkFlowLab#1253). Because the claim line now goes out after the save, that cue is not part of the save that announces the claim; a later write of the same card persists it as before. No reader of a card sees `claimCue` (`view()` strips it), and the dedupe still works in memory for a repeated announcement inside one process, so what changes is only whether the very next restart can compare against that saved cue. Posting a claim line for a card the store refused is the defect this PR fixes; a cue saved by a failed claim would be a lie about the room. ## Verification at the tip Verification commit (same tree as head): 798117eeda19314f3b52c29cab8720e673b65a10 Head: 5699bdb — tree 24dccee, the tree every run below was taken on; `.flybot_push.py` re-committed the verified tree on the live base. Ran: - red first on a clean `f2ef4dfaccedd40315bd585474d0902118dc0ba8` worktree carrying only `core/test/track-tasks-batch-refusal.test.js` from this branch (`core/src/host/tasks.js` at the base): `# tests 10 / # pass 3 / # fail 7` — the three ThinkFlowLab#2597 cases pass, the seven ThinkFlowLab#2761 cases fail: - `a refused save leaves a new batch out of state.tasks`: expected `{}`, actual both ghost cards (`Ghost card`, `Second ghost` working); - `a refused save leaves an updated card exactly as it was`: expected the stored card, actual `"title":"Renamed"`, `"note":"A note"`, `"status":"working"`, plus `"claimCue"`; - `a ghost card cannot ride an unrelated save onto the disk`: expected only the unrelated card on disk, actual the ghost id first; - `a refused save posts no claim line, a taken one posts exactly one`: expected 0 lines, actual 1; - `a refused claim leaves the queued card open`: actual `"status":"claimed"`, `"claimedBy":"d1"`, a lease and a worker; - `a refused release leaves the card claimed`: actual `"status":"open"`, `"retryAt":…`, `"queueReason":"needs a person"`; - `a refused rework opens no child card`: actual a `taskClass: 'rework'` child plus a rewritten `handoff` on the original. - after the fix, the same file on this tip: `# tests 10 / # pass 10 / # fail 0` - `TZ=UTC npm run verify` with `ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8`, on the committed tree `798117ee`: exit 0 — core `# tests 3143 / # pass 3143 / # fail 0`, social 619, tree 140, timeline 161, workflow 508, `.github/scripts` + policy 671, interop 37, live-e2e 34 - `ARCH_DOCS_BASE=f2ef4dfaccedd40315bd585474d0902118dc0ba8 npm --prefix core run arch-docs`, on the committed tree: `arch-docs: OK (6 changed file(s))` - `node scripts/ds-bot-patches.mjs generate` on this base wrote identical bytes; `check`: `10 patches apply cleanly in order`, `core/ equals ds-bot/ + patches/ds-bot/`; `canonical` after the commit: `ok patches/ds-bot/ is what generate writes (generate is a no-op)` - `node .github/scripts/changelog.mjs check`: `17 fragment(s) in changelog.d/ are well formed` Not run: `dev/bench.sh` smoke — no bench was started for this branch; the change is the task board's write path and is covered by the refusal suite plus the task suites. No real-model end-to-end run. ## How an old reader sees it No API shape, field name or stored value changes. A successful write behaves exactly as before, `save()` still runs once per batch, and a card that landed is unchanged. Only a write the store refuses now answers the same `Not saved:` error with the board untouched instead of mutated. The claim line is posted after the save rather than before it, which is the only ordering change. ## Dedupe No other open issue or PR covers this face; ThinkFlowLab#2597/ThinkFlowLab#2602 are the closed validation half before `save()`, and this is the store-refusal half on the same batch. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 2763, "claim": "Dev-1", "issue": 2761, "summary": "track_tasks committed the batch into state.tasks before save(), so a refused store write left cards the file never took, and the next unrelated save persisted them; a refused claim also posted its room line. The five write paths now snapshot the board (deep, because the claim, release and rework paths mutate the live card), restore it byte-identically on a refusal and rethrow, leave state.revision where the store moved it, and announce the claim only after the save. Seven new cases are red on a clean base and green after the fix.", "files": [ "core/src/host/tasks.js", "core/test/track-tasks-batch-refusal.test.js", "docs/subsystems/core-host.md", "changelog.d/2761-track-tasks-rollback.fixed.md", "patches/ds-bot/files/src/host/tasks.js", "patches/ds-bot/files/test/track-tasks-batch-refusal.test.js" ], "checks": { "ran": "TZ=UTC npm run verify with ARCH_DOCS_BASE=f2ef4dfa; core arch-docs; node scripts/ds-bot-patches.mjs generate/check/canonical; node .github/scripts/changelog.mjs check", "result": "green", "at": "5699bdb466204e896fef959c312010da155220c0" }, "next": "waiting for the review side's Verdict naming 5699bdb" } ``` <!-- /flybot:checkpoint -->
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Written-by: Dev
Fixes #2597
Owner: Dev
Reviewer: Reviewer
Tip SHA: 7ca7534
Base: 653cc05 (fetched
origin/main;git diff origin/main...HEAD --statlists only the five files below)What changed
track_tasksran its items one at a time and everymakeTask()put the new card intostore()immediately, while the batchsave()ran only after the loop. A later item's refusal — theask+column: 'done'pair, a queue already at its cap, a column the card cannot move to — unwound with the earlier cards already on the board, so the caller was told the batch had failed while those cards stayed, and a claim line could already have reached the room.The list now runs against a staged copy of the board; the copy is committed only after the last item passed, and claim lines are posted after that commit. A refused batch leaves
state.tasksbyte-identical, creates no card, and saves nothing. Every per-item refusal is unchanged and fires in the same order against the same state — the loop body is the same code, only its board is the copy. The generated mirrors underpatches/ds-bot/files/are updated bynode scripts/ds-bot-patches.mjs generate(CI'sds-bot-patches checkrequires them).Red first
core/test/track-tasks-batch-refusal.test.jsat the pre-change source (main@653cc056):Reverting the source change turns tests 1 and 2 red again with the same output; the accepted-batch control stays green either way. Green at the tip: 3/3.
Ran at the tip
TZ=UTC, on the pushed tip7ca75347c74229558bdedda8f9aed0ee622f05c2(downloaded from the branch ref, not the working copy):cd core && node --import ./test/harness-resolve.mjs --test test/track-tasks-batch-refusal.test.js— 3 tests, 3 pass, 0 fail.cd core && node --import ./test/harness-resolve.mjs --test test/tasks.test.js— 37 tests, 37 pass, 0 fail.831852526870538c7bf72e12b85cb12f51bd21aaequals the pushed tip's tree, so these are the same files):test/task-brief,task-route,task-queue,task-contract,task-audit,task-drive,bot-tasks,inbox-task-status,task-handoff-route,task-counters-updated-at,task-update-partial-write,task-queue-lease— all pass.cd core && npm run lint— 0 errors (84 pre-existing warnings),arch-docs: OK (5 changed file(s)).node scripts/ds-bot-patches.mjs check— 10 patches apply cleanly,core/equalsds-bot/+ patches.node .github/scripts/changelog.mjs check— 9 fragments well formed.npm run verify— the host runs out of threads when two suites run at once (the brief's budget); no client source was touched, soscripts/build-client.mjs --checkwas not run.Checkpoint
{ "pr": 2602, "claim": "Dev", "issue": 2597, "summary": "A track_tasks batch that ends in a refusal no longer leaves cards on the board. The whole list now runs against a staged copy of the board and the copy is committed only after every item passed, so a refused batch creates no card, changes none and saves nothing. Claim lines are posted after that commit, so a card that never landed is never announced. Every per-item refusal is unchanged and fires in the same order.", "files": ["core/src/host/tasks.js", "core/test/track-tasks-batch-refusal.test.js", "patches/ds-bot/files/src/host/tasks.js", "patches/ds-bot/files/test/track-tasks-batch-refusal.test.js", "changelog.d/2597.fixed.md"], "checks": { "ran": "cd core && node --import ./test/harness-resolve.mjs --test test/track-tasks-batch-refusal.test.js; node --import ./test/harness-resolve.mjs --test test/tasks.test.js", "result": "green", "at": "7ca75347c74229558bdedda8f9aed0ee622f05c2" }, "next": "none — ready for review" }Docs-not-needed: validation ordering inside an existing handler; no subsystem behaviour changes